/***************************************************************************
 *   Copyright (C) 2011-2015 by Tomasz Bojczuk                             *
 *   tomaszbojczuk@gmail.com                                               *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 3 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *  You should have received a copy of the GNU General Public License      *
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.  *
 ***************************************************************************/


#include "texam.h"
#include "tlevel.h"
#include "tattempt.h"
#include "tinitcorelib.h"
#include <tscoreparams.h>
#include <QFile>
#include <QDataStream>
#include <QMessageBox>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QSettings>



/*static*/
/** Versions history:
 * 1. 0x95121702; 
 * 
 * 2. 0x95121704; (2012.07)
 * 		- exam stores penalties in the list
 *
 * 3. 0x95121706 (2013.12.02)
 * 		- new level version
 * 
 * 4. 0x95121708 (2014.09.01)
 *    - encoded XML structure - let's hope universal
 */

const qint32 Texam::examVersion = 0x95121702;
const qint32 Texam::examVersion2 = 0x95121704;
const qint32 Texam::currentVersion = 0x95121708; // version 4

const quint16 Texam::maxAnswerTime = 65500;

int Texam::examVersionNr(qint32 ver) {
	if ((ver - examVersion) % 2)
			return -1; // invalid when rest of division is 1
	return ((ver - examVersion) / 2) + 1 ;
}


bool Texam::couldBeExam(qint32 ver) {
	int givenVersion = examVersionNr(ver);
	if (givenVersion >= 1 && givenVersion <= 127)
		return true;
	else
		return false;
}


bool Texam::isExamVersion(qint32 ver) {
	if (examVersionNr(ver) <= examVersionNr(currentVersion))
		return true;
	else
		return false;
}

/** Obsolete! It has no meaning in XML versions (above 3) */
qint32 Texam::examVersionToLevel(qint32 examVer) {
	if (examVersionNr(examVer) <= 2)
		return Tlevel::getVersionId(1); // level version 1 for exam versions 1 and 2
	else
		return Tlevel::getVersionId(2); // level version 2 for exam versions 3 and so
}


bool Texam::areQuestTheSame(TQAunit* q1, TQAunit* q2) {
  if (q1->questionAs == q2->questionAs && // the same questions
      q1->answerAs == q2->answerAs && // the same answers
      q1->qa.note == q2->qa.note && // the same notes
      q1->qa.pos == q2->qa.pos // the same frets
		 )
			return true;
  else
			return false;
}


QString Texam::formatReactTime(quint16 timeX10, bool withUnit) {
		QString hh = "", mm = "", ss = "";
    int dig = 0;
    if (timeX10 / 36000) {
        hh = QString("%1").arg(timeX10 / 36000);
        dig = 2;
    }
    int dig2 = 0;
    if ((timeX10 % 36000) / 600) {
        mm = QString("%1").arg((timeX10 % 36000) / 600, dig, 'i', 0, '0');
        dig2 = 2;
    }
    ss = QString("%1").arg(((timeX10 % 36000) % 600) / 10, dig2, 'i', 0, '0' );
    QString res = "";
    if (hh != "")
        res = hh + ":";
    if (mm != "")
        res += mm + ":";
    QString unitS = "";
    if (withUnit && timeX10 < 600)
        unitS = " s";
    return res + ss + QString(".%1").arg(timeX10 % 10) + unitS;
}

/*end of static*/

//#################################################################################################
//###################             CONSTRUCTOR          ############################################
//#################################################################################################
Texam::Texam(Tlevel* l, QString userName):
  m_userName(userName),
  m_fileName(""),
  m_mistNr(0), m_tmpMist(0),
  m_averReactTime(0),
  m_workTime(0), m_totalTime(0),  m_attempts(0),
  m_penaltysNr(0),
  m_isFinished(false), m_melody(false), m_isExercise(false),
  m_halfMistNr(0), m_tmpHalf(0),
  m_blackCount(0),
	m_okTime(0),
	m_effectivenes(0.0),
	m_skippedUnit(0)
{
	setLevel(l);
}


Texam::~Texam()
{
  clearAnswList();
  m_blackList.clear();
	m_blackNumbers.clear();
  if (m_skippedUnit)
    delete m_skippedUnit;
}

//#################################################################################################
//###################                PUBLIC            ############################################
//#################################################################################################
void Texam::setExercise() {
	if (count()) {
		qDebug() << "Exam has got questions already. Can't set it as an exercise!";
		return;
	}
	setFileName(QDir::toNativeSeparators(QFileInfo(Tcore::gl()->config->fileName()).absolutePath() + "/exercise.noo"));	
	m_isExercise = true;
}


void Texam::setLevel(Tlevel* l) {
	m_level = l;
	m_melody = l->canBeMelody();
}


void Texam::setFileName(const QString& fileName) {
	if (isExercise()) {
		qDebug() << "Can not set a file name for exercise";
		return;
	}
	m_fileName = fileName;
}


void Texam::skipLast(bool skip) {
  if (skip != (bool)m_skippedUnit) {
    if (skip) {
      if (m_skippedUnit)
        qDebug() << "Previously skipped question unit will be overridden by newly skipped.";
      delete m_skippedUnit;
      m_skippedUnit = m_answList.takeLast();
    } else {
      if (!m_skippedUnit)
        qDebug() << "There is no skipped unit to revert it back!";
      else {
        m_answList << m_skippedUnit;
        m_skippedUnit = 0;
      }
    }
  }
}


Texam::EerrorType Texam::loadFromFile(QString& fileName) {
	m_okTime = 0;
	m_tmpMist = 0;
	m_tmpHalf = 0;
	m_fileName = fileName;
	QFile file(fileName);
	m_workTime = 0;
	m_mistNr = 0;
	m_blackCount = 0;
	m_attempts = 0;
  m_isExercise = false;
	m_blackList.clear();
	clearAnswList();
	EerrorType result = e_file_OK;
	quint32 ev; //exam template version
	if (file.open(QIODevice::ReadOnly)) {
		QDataStream in(&file);
		in >> ev;
		if (couldBeExam(ev)) {
			if (!isExamVersion(ev))
					return e_newerVersion;
		}	else
				return e_file_not_valid;
		
		bool isExamFileOk = true;
		if (examVersionNr(ev) == 4) {
			in.setVersion(QDataStream::Qt_5_2);
			QByteArray arrayXML = file.readAll();
			arrayXML.remove(0, 4);
			QByteArray unZipXml = qUncompress(arrayXML);
			if (!unZipXml.isEmpty()) {
				QXmlStreamReader xml(unZipXml);
				isExamFileOk = loadFromXml(xml);
			} else {
				qDebug() << "Problems with decompressing exam file";
				return e_file_not_valid;
			}					
		} else {
				in.setVersion(QDataStream::Qt_4_7);
				isExamFileOk = loadFromBin(in, ev);
		}
		
		m_melody = m_level->canBeMelody();
		updateEffectiveness();
		updateAverageReactTime(true);
// 		if ((count() - mistakes()))
// 				m_averReactTime = m_okTime / (count() - mistakes());
// 		else 
// 				m_averReactTime = 0.0;
		
		if (!isExamFileOk)
				result = e_file_corrupted;
		file.close();
	} else {
			Tlevel::fileIOerrorMsg(file, 0);
			result = e_cant_open;
	}
  updateBlackCount();
  return result;
}


bool Texam::loadFromBin(QDataStream& in, quint32 ev) {
	quint16 questNr;
	bool isExamFileOk = true;
	in >> m_userName;
	getLevelFromStream(in, *(m_level), examVersionToLevel(ev));
	in >> m_tune;
	in >> m_totalTime;
	in >> questNr >> m_averReactTime >> m_mistNr;
	if (examVersionNr(ev) >= 2) {
		in >> m_halfMistNr >> m_penaltysNr >> m_isFinished;
	} else { // exam version 1
			m_halfMistNr = 0;
			m_penaltysNr = 0;
			m_isFinished = false;
	}
	while (!in.atEnd()) {
		TQAunit qaUnit;
		if (!getTQAunitFromStream(in, qaUnit))
				isExamFileOk = false;
		if ((qaUnit.questionAs == TQAtype::e_asName || qaUnit.answerAs == TQAtype::e_asName) 
			&& qaUnit.styleOfQuestion() < 0) {
				qaUnit.setStyle(Tcore::gl()->S->nameStyleInNoteName, qaUnit.styleOfAnswer());
		} /** In old versions, style was set to 0 so now it gives styleOfQuestion = -1
			* Also in transition Nootka versions it was left unchanged.
			* Unfixed it invokes stupid names in charts.
			* We are fixing it by insert user preferred style of naming */
		if (qaUnit.time <= maxAnswerTime || ev == examVersion) { // add to m_answList
				m_answList << new TQAunit(qaUnit);
				grabFromLastUnit();
		} else { // add to m_blackList
				m_blackList << qaUnit;
		}
	}
	if (!checkQuestionNumber(questNr)) {
		isExamFileOk = false;        
	}
	if (examVersionNr(ev) >= 2 && (m_tmpMist != m_mistNr || m_tmpHalf != m_halfMistNr)) {
		m_mistNr = m_tmpMist; // we try to fix exam file to give proper number of mistakes
		m_halfMistNr = m_tmpHalf;
		isExamFileOk = false;
	} else {
		m_mistNr = m_tmpMist; // transition to exam version 2
	}
	if (ev == examVersion) {
			convertToVersion2();
			m_halfMistNr = m_tmpHalf;
	}
	return isExamFileOk;
}



bool Texam::loadFromXml(QXmlStreamReader& xml) {
	bool ok = true;
	int questNr = 0;
	int fixedNr = 0;
	if (xml.readNextStartElement()) {
		if (xml.name() != "exam") {
			qDebug() << "There is no 'exam' key in that XML";
			return false;
		}
		m_userName = xml.attributes().value("user").toString();
		if (m_userName.isEmpty() || m_userName.size() > 30) {
			qDebug() << "Exam has wrong user name";
			return false;
		}
	}
	while (xml.readNextStartElement()) {
// 		qDebug() << "exam" << xml.name();
		if (xml.name() == "head") {
			while (xml.readNextStartElement()) {
// 				qDebug() << "head" << xml.name();
				if (xml.name() == "level") {
					Tlevel::EerrorType err = m_level->loadFromXml(xml);
					if (err != Tlevel::e_level_OK) {
						qDebug() << "Exam has wrong level definition" << (int)err;
						ok = false;
					}
				} else if (xml.name() == "tuning") {
					if (!m_tune.fromXml(xml, true)) {
						qDebug() << "Exam has wrong tuning";
						ok = false;
					}
				} else if (xml.name() == "totalTime")
					m_totalTime = xml.readElementText().toInt();
				else if (xml.name() == "questNr")
					questNr = xml.readElementText().toInt();
				else if (xml.name() == "averReactTime")
					m_averReactTime = xml.readElementText().toInt();
				else if (xml.name() == "mistNr")
					m_mistNr = xml.readElementText().toInt();
				else if (xml.name() == "halfMistNr")
					m_halfMistNr = xml.readElementText().toInt();
				else if (xml.name() == "penaltysNr")
					m_penaltysNr = xml.readElementText().toInt();
				else if (xml.name() == "finished")
					m_isFinished = QVariant(xml.readElementText()).toBool();
        else if (xml.name() == "exercise") {
          m_isExercise = true;
					xml.skipCurrentElement();
				} else
					Tlevel::skipCurrentXmlKey(xml);
			}
		} else if (xml.name() == "answers") {
				if (!readAnswerFromXml(m_answList, xml))
					ok = false;
		} else if (xml.name() == "penalties") {
				if (!readPenaltyFromXml(m_blackList, xml))
						ok = false;
		} else if (xml.name() == "black") {
				m_blackNumbers.clear();
				while (xml.readNextStartElement()) {
				  if (xml.name() == "n")
						m_blackNumbers << xml.readElementText().toInt();
					else
						Tlevel::skipCurrentXmlKey(xml);
				}
		} else
				Tlevel::skipCurrentXmlKey(xml);
	}
	if (!checkQuestionNumber(questNr)) {
		ok = false;        
	}
	if (m_tmpMist != m_mistNr || m_tmpHalf != m_halfMistNr) {
		if (m_tmpMist != m_mistNr)
			qDebug() << "Mistakes number do not match. Exam file corrupted!" << m_tmpMist << m_mistNr;
		else if (m_tmpHalf != m_halfMistNr)
			qDebug() << "'Not bad' number do not match. Exam file corrupted!" << m_tmpHalf << m_halfMistNr;
		m_mistNr = m_tmpMist; // we try to fix exam file to give proper number of mistakes
		m_halfMistNr = m_tmpHalf;
		ok = false;
	}
	return ok;
}


Texam::EerrorType Texam::saveToFile(QString fileName) {
	if (fileName != "")
		setFileName(fileName); // m_fileName becomes fileName
	if (m_fileName == "")
		return e_noFileName;
	QFile file(m_fileName);
	if (file.open(QIODevice::WriteOnly)) {
		QDataStream out(&file);
		out.setVersion(QDataStream::Qt_5_2);
		out << currentVersion;
		QByteArray arrayXML;
		QXmlStreamWriter xml(&arrayXML);
// 		xml.setAutoFormatting(true);
		xml.writeStartDocument();
		xml.writeComment("\nXML file of Nootka exam data.\nhttp://nootka.sf.net\nThis file should never be opened in other software then Nootka.\nProbably you are doing something illegal!");
		writeToXml(xml);
		xml.writeEndDocument();
		
		out << qCompress(arrayXML);
// 		out << arrayXML;
		
		file.close();
	} else {
		QMessageBox::critical(0, "",
           QObject::tr("Cannot save exam file:\n%1").arg(QString::fromLocal8Bit(qPrintable(file.errorString()))));
		return e_cant_open;
	}
	qDebug() << "Exam saved to:" << m_fileName;
	return e_file_OK;
}


void Texam::writeToXml(QXmlStreamWriter& xml) {
	xml.writeStartElement("exam");
		xml.writeAttribute("user", m_userName);
		xml.writeStartElement("head");
			m_level->writeToXml(xml);
			m_tune.toXml(xml, true);
			xml.writeTextElement("totalTime", QVariant(m_totalTime).toString());
			xml.writeTextElement("questNr", QVariant(count()).toString());
			xml.writeTextElement("averReactTime", QVariant(m_averReactTime).toString());
			xml.writeTextElement("mistNr", QVariant(m_mistNr).toString());
			xml.writeTextElement("halfMistNr", QVariant(m_halfMistNr).toString());
			xml.writeTextElement("penaltysNr", QVariant(m_penaltysNr).toString());
			xml.writeTextElement("finished", QVariant(m_isFinished).toString());
      if (isExercise())
        xml.writeEmptyElement("exercise");
		xml.writeEndElement(); // head
		xml.writeStartElement("answers");
		for (int i = 0; i < count(); ++i)
			m_answList[i]->toXml(xml);
		xml.writeEndElement(); // answers
		if (m_blackList.size()) {
			xml.writeStartElement("penalties");
			for (int i = 0; i < m_blackList.size(); ++i)
				m_blackList[i].toXml(xml);
			xml.writeEndElement(); // penalties
		} else if (m_blackNumbers.size()) {
			xml.writeStartElement("black");
			for (int i = 0; i < m_blackNumbers.size(); ++i)
				xml.writeTextElement("n", QString::number(m_blackNumbers[i]));
			xml.writeEndElement(); // penalties
		}
	xml.writeEndElement(); // exam
}


void Texam::newAttempt() {
  curQ()->newAttempt();
  if (curQ()->attemptsCount() > 1) { // unset answered and revert mistakes - user tries once more
    if (curQ()->isNotSoBad())
      m_halfMistNr--;
    else if (curQ()->isWrong())
      m_mistNr--;
    else
      qDebug() << "new attempt called for correct answer!";
    curQ()->unsetAnswered();
  }
}


void Texam::sumarizeAnswer() {
	curQ()->updateEffectiveness();
	curQ()->time = qMin(maxAnswerTime, curQ()->time); // when user think too much
	if (melodies()) {
		m_workTime += curQ()->lastAttempt()->totalTime();
		if (!curQ()->isWrong()) {
				if (curQ()->effectiveness() < 50)
          curQ()->setMistake(TQAunit::e_veryPoor);
				else if (curQ()->effectiveness() < 70)
          curQ()->setMistake(TQAunit::e_poorEffect);
		}
		m_attempts++;
	}
	updateAverageReactTime(true);
	if (melodies()) {
    if (curQ()->isNotSoBad())
      m_halfMistNr++;
    else if (curQ()->isWrong())
      m_mistNr++;
  } else {
    addPenalties(); // for melodies it should be invoked after ensuring that answer was finished
    if (!isExercise())
      updateBlackCount();
    m_workTime += curQ()->time;
	}
	updateEffectiveness();
}


void Texam::addPenalties() {
	if (!curQ()->isCorrect()) {
		if (melodies())
				m_blackNumbers.append(-1); // one more random melody
		if (curQ()->isNotSoBad()) {
			if (!isExercise() /*&& !melodies() */&& !isFinished())
					m_penaltysNr++;
      if (!melodies())
        m_halfMistNr++;
		} else {
			if (melodies())
				m_blackNumbers.append(count() - 1); // repeat current melody in some further question
			if (!isExercise() /*&& !melodies() */&& !isFinished())
					m_penaltysNr += 2;
      if (!melodies())
        m_mistNr++;
		}
	}
}


void Texam::updateEffectiveness() {
	qreal sum = 0.0;
	for (int i = 0; i < count(); ++i)
		sum += answList()->at(i)->effectiveness();
	m_effectivenes = sum / (qreal)count();
}


void Texam::updateAverageReactTime(bool skipWrong) {
	int totalTime = 0;
  int cnt = 0;
	for (int i = 0; i < count(); ++i) {
		if (!skipWrong || (skipWrong && !m_answList[i]->isWrong())) {
			totalTime += m_answList[i]->time;
      cnt++;
    }
	}
	if (cnt)
    m_averReactTime = totalTime / cnt;
  else
    m_averReactTime = 0;
}

//#################################################################################################
//###################              PROTECTED           ############################################
//#################################################################################################

void Texam::updateBlackCount() {
  m_blackCount = 0;
  if (m_blackList.size()) {
    for (int i = 0; i < m_blackList.size(); i++)
			m_blackCount += (m_blackList[i].time - maxAnswerTime);
  }
}


bool Texam::readPenaltyFromXml(QList<TQAunit>& blackList, QXmlStreamReader& xml) {
  bool ok = true;
  while (xml.readNextStartElement()) {
    if (xml.name() == "u") {
      blackList << TQAunit(this);
      if (blackList.last().fromXml(xml)) {
        grabFromLastUnit();
        if (melodies())
          m_attempts += curQ()->attemptsCount();
      } else {
        qDebug() << "Exam has wrong unit" << blackList.size();
        blackList.removeLast();
        ok = false;
      }
    } else
        Tlevel::skipCurrentXmlKey(xml);
  }
  return ok;
}


bool Texam::readAnswerFromXml(QList<TQAunit*>& list, QXmlStreamReader& xml) {
	bool ok = true;
	while (xml.readNextStartElement()) {
		if (xml.name() == "u") {
			list << new TQAunit(this);
			if (list.last()->fromXml(xml)) {
				grabFromLastUnit();
				if (melodies())
					m_attempts += curQ()->attemptsCount();
			} else {
				qDebug() << "Exam has wrong unit" << list.size();
				list.removeLast();
				ok = false;
			}
		} else
				Tlevel::skipCurrentXmlKey(xml);
	}
	return ok;
}


void Texam::grabFromLastUnit() {
	m_workTime += curQ()->time;
	if (!curQ()->isCorrect()) {
		if (curQ()->isWrong())
				m_tmpMist++;
		else
				m_tmpHalf++; // not so bad answer
		}
	if (!curQ()->isWrong())
		m_okTime += curQ()->time;
}


bool Texam::checkQuestionNumber(int questNr) {
	bool ok = true;
	if (questNr != m_answList.size()) {
		qDebug() << "Exam questions number read from file" << questNr << 
		"and those calculated" << m_answList.size() << "do not match. Exam file corrupted.";
		ok = false;
	}
	return ok;
}


void Texam::clearAnswList() {
  for (int i = 0; i < m_answList.size(); ++i)
    delete m_answList[i];
  m_answList.clear();
}


/** This method exist for backward compatibility but is has rate use in 'modern' Nootka times  */
void Texam::convertToVersion2() {
  bool hasStyle = false;
  Tnote::EnameStyle randStyles[3];
  if (m_level->canBeName()) {
    // version 1 didn't put proper Tnote::EnameStyle to file - we fixing it
    hasStyle = true;
    qDebug("Fixing styles of note names in file");
    qsrand(QDateTime::currentDateTime().toTime_t());
   if (m_level->requireStyle) { // prepare styles array to imitate switching
      randStyles[0] = Tnote::e_italiano_Si;
      if (Tcore::gl()->S->seventhIs_B) {
        randStyles[1] = Tnote::e_english_Bb;
        randStyles[2] = Tnote::e_nederl_Bis;
      } else {
        randStyles[1] = Tnote::e_norsk_Hb;
        randStyles[2] = Tnote::e_deutsch_His;
      }
   }
  }
  
  for (int i = 0; i < m_answList.size(); i++) {
    if (m_answList[i]->time > maxAnswerTime) // fix too long times from version 1 if any
        m_answList[i]->time = maxAnswerTime;
  // version 1 didn't put proper Tnote::EnameStyle to file - we fixing it
    if (hasStyle) {
      if (m_level->requireStyle) {
        if (m_answList[i]->questionAs == TQAtype::e_asName && m_answList[i]->answerAs == TQAtype::e_asName) {
          Tnote::EnameStyle qSt = randStyles[qrand() % 3];
          Tnote::EnameStyle aSt;
          if (qSt == Tnote::e_italiano_Si)
            aSt = randStyles[(qrand() % 2) +1];
          else
            aSt = Tnote::e_italiano_Si;
          m_answList[i]->setStyle(qSt, aSt);
        } else
          if (m_answList[i]->questionAs == TQAtype::e_asName) {
            m_answList[i]->setStyle(randStyles[qrand() % 3], Tcore::gl()->S->nameStyleInNoteName);
          } else
            if (m_answList[i]->questionAs == TQAtype::e_asName) {
              m_answList[i]->setStyle(Tcore::gl()->S->nameStyleInNoteName, randStyles[qrand() % 3]);
            }
      } else // fixed style - we changing to user preferred
          m_answList[i]->setStyle(Tcore::gl()->S->nameStyleInNoteName, Tcore::gl()->S->nameStyleInNoteName);
    }
      
    if (!m_answList[i]->isCorrect()) {
      quint16 penCnt = 0; // counts of penalties
      if (m_answList[i]->isWrong()) {
        if (i < (m_answList.size() -1) && areQuestTheSame(m_answList[i], m_answList[i+1])) {
          // there was next question repeated
          if (m_answList[i+1]->isCorrect()) // and was correct
            penCnt = 65501; // so add one penalty
          else // when again wrong
            penCnt = 65502; // add two
          // The next loop will add next two penalties !!
        } else // question was not repeated
            penCnt = 65502;
      } else { // not so bad
        if (i < (m_answList.size() -1) && areQuestTheSame(m_answList[i], m_answList[i+1])) {
          // there was next question repeated
          if (m_answList[i+1]->isCorrect()) // and was correct
//             m_blackList.removeLast(); // remove it from black list - corrected
            penCnt = 0;
          else
            penCnt = 65501;
        }
      }
      if (penCnt) {
        m_blackList << *m_answList[i];
        m_blackList.last().time = penCnt;
        m_penaltysNr += (penCnt - 65500);
      }
    }
  }
//   qDebug() << "Converted to exam version 2!!!  black list:" << m_blackList.size() << "penaltys:" << m_penaltysNr;
}



